class Vue{
    constructor(options){
        this.$el = options.el
        this.$data = options.data
        let computed = options.computed
        let methods = options.methods
        if(this.$el){
            new Observer(this.$data)
            this.proxyMethods(methods)
            this.proxyComputed(computed)
            this.proxyData(this.$data)
            new Compiler(this.$el,this)
        }
    }
    proxyMethods(methods){
        for(let key in methods){
            Object.defineProperty(this,key,{
                get:()=>{
                    return methods[key]
                }
            })
        }
    }

    proxyComputed(computed){
        for(let key in computed){
            Object.defineProperty(this.$data,key,{
                get:()=>{
                    return computed[key].call(this)
                }
            })
        }
    }

    proxyData(data){
        for(let key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                },
                set(newValue){
                    data[key] = newValue
                }
            })
        }
    }
}

class Dep{
    constructor(){
        this.subs = []
    }
    add(watcher){
        this.subs.push(watcher)
    }
    notify(){
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}
class Watcher{
    constructor(vm,expr,cb){
        this.vm = vm 
        this.expr = expr
        this.cb = cb
        Dep.target = this
        this.oldValue = this.getVal()
        Dep.target = null
    }
    getVal(){
        return CompileUtil.getVal(this.expr,this.vm)
    }
    update(){
        let newValue = this.getVal()
        if(newValue != this.oldValue){
            this.cb(newValue)
        }
    }
}


class Observer{
    constructor(data){
        this.observer(data)
    }
    observer(data){
        if(data && typeof(data) === 'object'){
            for(let key in data){
                this.defineReactive(data,key,data[key])
            }
        }
    }
    defineReactive(obj,key,value){
        this.observer(value)
        let dep = new Dep()
        Object.defineProperty(obj,key,{
            get(){
                Dep.target && dep.add(Dep.target)
                return value
            },
            set:(newValue) => {
                if(newValue !== value){
                    this.observer(newValue)
                    value = newValue
                    dep.notify()
                }
            }
        })
    }
}


class Compiler{
    constructor(el,vm){
        this.vm = vm
        this.el = el.nodeType === 1 ? el : document.querySelector(el)
        let fragment = this.nodeToFragment(this.el)
        this.compile(fragment)
        this.el.appendChild(fragment)
    }
    nodeToFragment(node){
        let fragment = document.createDocumentFragment()
        let firstChild
        while(firstChild = node.firstChild){
            fragment.appendChild(firstChild)
        }
        return fragment
    }
    compile(node){
        let childNodes = node.childNodes
        ;[...childNodes].forEach(child => {
            if(child.nodeType === 1){
                this.compileElement(child)
                this.compile(child)
            }else{
                this.compileText(child)
            }
        })
    }
    compileElement(node){
        let attributes = node.attributes
        ;[...attributes].forEach(attr => {
            let {name,value:expr} = attr
            if(name.startsWith('v-')){
                let directive = name.split("-")[1]
                let [directName,eventName] = directive.split(":")
                CompileUtil[directName](node,expr,this.vm,eventName)
            }
        })
    }
    compileText(node){
        let content = node.textContent
        if(/\{\{(.+?)\}\}/.test(content)){
            CompileUtil["text"](node,content,this.vm)
        }
    }
}

CompileUtil = {
    model(node,expr,vm){
        let fn = this.updater.modelUpdater
        new Watcher(vm,expr,newValue => {
            fn(node,newValue)
        })
        let value = this.getVal(expr,vm)
        fn(node,value)
        
        node.addEventListener('input',e => {
            let inputVal = e.target.value
            this.setVal(expr,inputVal,vm)
        })
    },
    on(node,fun,vm,event){
        node.addEventListener(event,(e) => {
            vm[fun](e)
        })
    },
    html(node,expr,vm){
        let fn = this.updater.htmlUpdater
        new Watcher(vm,expr,newValue => {
            fn(node,newValue)
        })
        let content = this.getVal(expr,vm)
        fn(node,content)
    },
    text(node,content,vm){
        let fn = this.updater.textUpdater
        let newContent = content.replace(/\{\{(.+?)\}\}/g,(...args) => {
            new Watcher(vm,args[1],() => {
                let ncontent = content.replace(/\{\{(.+?)\}\}/g,(...args) => this.getVal(args[1],vm))
                fn(node,ncontent)
            })
            return this.getVal(args[1],vm)
        })
        fn(node,newContent)
    },
    updater:{
        modelUpdater(node,value){
            node.value = value
        },
        textUpdater(node,content){
            node.textContent = content
        },
        htmlUpdater(node,content){
            node.innerHTML = content
        }
    },
    getVal(expr,vm){
        return expr.split('.').reduce((obj,item) => {
            return obj[item]
        },vm.$data)
    },
    setVal(expr,value,vm){
        console.log(value)
        expr.split('.').reduce((obj,item,index,arr) => {
            if(index == arr.length-1){
                obj[item] = value
            }
            return obj[item]
        },vm.$data)
    }
}

